<!--
 * @Author: your name
 * @Date: 2021-04-30 11:19:50
 * @LastEditTime: 2021-05-07 10:54:51
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /large-fill-upload/src/view/fileUpload/index.vue
-->
<template>
  <div style="width: 1000px; margin: 20px auto 0">
    <div style="display: flex; justify-content: center">
      <input type="file" @change="handleFileChange" />
      <el-button @click="handleUpload">上传</el-button>
      <el-button @click="handlePause" v-if="isPaused">暂停</el-button>
      <el-button @click="handleResume" v-else>恢复</el-button>
    </div>
    <div class="percentage">
      <!-- 总进度 -->
      <el-progress title="hash进度" :percentage="hashPercentage"></el-progress>
      <el-progress
        title="总进度"
        :percentage="fakeUploadPercentage"
      ></el-progress>

      <el-table :data="data" style="width: 1000px">
        <el-table-column prop="hash" label="切片hash"></el-table-column>
        <el-table-column prop="chunk" label="大小(KB)">
          <template slot-scope="scope">{{ getSizeKB(scope.row) }}</template>
        </el-table-column>
        <el-table-column prop="percentage" label="进度" width="400px">
          <template scope="scope">
            <el-progress :percentage="scope.row.percentage"></el-progress>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script>
const SIZE = 1 * 1024 * 1024; // 切片大小
export default {
  data: () => ({
    container: {
      file: null,
    },
    data: [],
    getSizeKB(row) {
      return parseInt(row.chunk.size / 1024) || 0;
    },
    isPaused: true,
    fakeUploadPercentage: 0, // 总进度
    requestList: [],
    hashPercentage: 0, // hash进度
  }),
  watch: {
    uploadPercentage(now) {
      if (now > this.fakeUploadPercentage) {
        this.fakeUploadPercentage = now;
      }
    },
  },
  computed: {
    // 总进度
    uploadPercentage() {
      if (!this.container.file || !this.data.length) return 0;
      const loaded = this.data
        .map((item) => item.chunk.size * item.percentage)
        .reduce((acc, cur) => acc + cur);
      return parseInt((loaded / this.container.file.size).toFixed(2));
    },
  },
  methods: {
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) return;
      // debugger;
      Object.assign(this.$data, this.$options.data());
      this.container.file = file;
    },
    // 暂停
    handlePause() {
      console.log(this.requestList, "this.requestList");
      this.requestList.forEach((xhr) => xhr?.abort());

      this.requestList = [];
      this.isPaused = false;
    },
    // 恢复
    async handleResume() {
      this.isPaused = true;
      const { uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      await this.uploadChunks(uploadedList);
    },
    request({
      url,
      method = "post",
      data,
      headers = {},
      onProgress = (e) => e,
      requestList,
    }) {
      let baseUrl = `http://localhost:3000/`;
      return new Promise((resolve) => {
        const xhr = new XMLHttpRequest();
        xhr.upload.onprogress = onProgress;
        xhr.open(method, baseUrl + url);
        Object.keys(headers).forEach((key) =>
          xhr.setRequestHeader(key, headers[key])
        );
        xhr.send(data);
        xhr.onload = (e) => {
          // 将请求成功的 xhr 从列表中删除
          if (requestList) {
            const xhrIndex = requestList.findIndex((item) => item === xhr);
            requestList.splice(xhrIndex, 1);
          }

          resolve({
            data: e.target.response,
          });
        };
        // 暴露当前 xhr 给外部
        requestList?.push(xhr);
      });
    },
    // 生成文件切片
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },
    // 上传切片，同时过滤已上传的切片
    async uploadChunks(uploadedList = []) {
      const requestList = this.data
        .filter(({ hash }) => !uploadedList.includes(hash)) // 过滤已上传的hash切片
        .map(({ chunk, hash, index }) => {
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.hash);
          return { formData, index };
        })
        .map(async ({ formData, index }) =>
          this.request({
            url: "upload",
            data: formData,
            onProgress: this.createProgressHandler(this.data[index]),
            requestList: this.requestList,
          })
        );
      try {
        await Promise.all(requestList); // 并发切片
        // 合并切片
        if (uploadedList.length + requestList.length === this.data.length)
          await this.mergeRequest();
      } catch (e) {
        console.log(e);
      }
    },
    // 生成文件 hash（web-worker）
    calculateHash(fileChunkList) {
      return new Promise((resolve) => {
        // 添加 worker 属性
        this.container.worker = new Worker("/hash.js");
        this.container.worker.postMessage({ fileChunkList });
        this.container.worker.onmessage = (e) => {
          const { percentage, hash } = e.data;
          this.hashPercentage = percentage;
          if (hash) {
            resolve(hash);
          }
        };
      });
    },
    // 验证hash文件
    async verifyUpload(filename, fileHash) {
      const { data } = await this.request({
        url: "verify",
        headers: {
          "content-type": "application/json",
        },
        data: JSON.stringify({
          filename,
          fileHash,
        }),
      });
      return JSON.parse(data);
    },
    // 获取后缀
    getSuffix(str) {
      return str.substr(str.lastIndexOf(".")) || "";
    },
    async handleUpload() {
      if (!this.container.file) return;
      const fileChunkList = this.createFileChunk(this.container.file);
      this.container.hash = await this.calculateHash(fileChunkList);
      // 文件妙传：验证文件是否存在
      const { shouldUpload, uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      if (!shouldUpload) {
        this.$message.success("秒传：上传成功");
        return;
      }
      this.data = fileChunkList.map(({ file }, index) => ({
        // fileHash: this.container.hash,
        chunk: file,
        index,
        hash: this.container.hash + "-" + index, // 文件名  数组下标
        percentage: uploadedList?.includes(index) ? 100 : 0,
      }));
      await this.uploadChunks(uploadedList);
    },
    createProgressHandler(item) {
      return (e) => {
        item.percentage = parseInt(String((e.loaded / e.total) * 100));
      };
    },

    async mergeRequest() {
      let { data } = await this.request({
        url: "merge",
        headers: {
          "content-type": "application/json",
        },
        data: JSON.stringify({
          filename: this.container.hash,
          size: SIZE,
          suffix: this.getSuffix(this.container.file.name),
        }),
      });
      let { code, message } = JSON.parse(data);
      if (code == 0) {
        this.$message({
          message: message || "上传成功",
          type: "success",
        });
      }
    },
  },
};
</script>
